package dryven.model.binding.proxy;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import dryven.model.binding.fieldset.BoundField;

public class OldBindingProxyClassLoader extends ClassLoader {

	private Map<Class<?>, Object> _cachedStubs;
	
	public <T> Class<? extends T> defineProxyForClass(Class<T> clazz) {
		byte[] bytecode = generateByteCode(clazz);
		return (Class<? extends T>) defineClass(bytecode, 0, bytecode.length);
	}
	
	private byte[] generateByteCode(Class<?> currentSuperClass) {
		String fullSuperClassName = currentSuperClass.getName().replace('.', '/');
		String fullClassName = fullSuperClassName+"Dryven"+BindingProxy.class.getSimpleName()+"__";
		ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
		//public class ClassNameStub {
		writer.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, fullClassName, null, fullSuperClassName, null);
		
		//private BindingProxy _proxy;
		FieldVisitor fv = writer.visitField(Opcodes.ACC_PRIVATE, "_proxy", Type.getDescriptor(BindingProxy.class), null, null);
		fv.visitEnd();
		
		generateConstructor(currentSuperClass, writer, fullClassName);
		
		for (Method m : currentSuperClass.getMethods()) {
			if(m.getDeclaringClass().equals(Object.class) || Modifier.isStatic(m.getModifiers())) {
				continue;
			}
			MethodVisitor mv = writer.visitMethod(getAccessLevel(m), m.getName(), getMethodDescriptor(m), null, null);
			if(m.getReturnType().equals(Void.TYPE)) {
				generateSetterMethod(m, mv);
			}
			else {
				generateGetterMethod(m, mv);
			}
			mv.visitEnd();
		}
		
		//} end class
		writer.visitEnd();
		
		return writer.toByteArray();
	}

	private void generateConstructor(Class<?> currentSuperClass,
			ClassWriter writer, String fullClassName) {
		//first find the constructor with the least amount of arguments
		int minArgumentCount = Integer.MAX_VALUE;
		Constructor<?> minCtor = null;
		for (Constructor<?> ctor : currentSuperClass.getDeclaredConstructors()) {
			if(ctor.getParameterTypes().length<minArgumentCount) {
				minArgumentCount = ctor.getParameterTypes().length;
				minCtor = ctor;
			}
		}
		Class<?> ctorParams[] = minCtor.getParameterTypes();
		/*
		MethodVisitor cv = writer.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "("+Type.getDescriptor(BindingProxy.class)+")V", null, null);
		cv.visitCode();
		cv.visitVarInsn(Opcodes.ALOAD, 0);
		cv.visitMethodInsn(Opcodes.INVOKESPECIAL, currentSuperClass.getName().replace('.', '/'), "<init>", "()V");
		cv.visitInsn(Opcodes.RETURN);
		cv.visitMaxs(1, 1);
		cv.visitEnd();*/
		
		//public ClassNameStub(BindingProxy proxy) {
		MethodVisitor mv = writer.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "("+Type.getDescriptor(BindingProxy.class)+")V", null, null);
		mv.visitCode();
		mv.visitVarInsn(Opcodes.ALOAD, 0);
		StringBuilder ctorSignature = new StringBuilder();
		for(int i=0;i<ctorParams.length;++i) {
			Class<?> paramType = ctorParams[i];
			ctorSignature.append(Type.getDescriptor(paramType));
			pushTypeDefaultValueAndReturnOpCode(mv, paramType);
		}
		mv.visitMethodInsn(Opcodes.INVOKESPECIAL, currentSuperClass.getName().replace('.', '/'), "<init>", "("+ctorSignature.toString()+")V");
		mv.visitVarInsn(Opcodes.ALOAD, 0);
		mv.visitVarInsn(Opcodes.ALOAD, 1);
		mv.visitFieldInsn(Opcodes.PUTFIELD, fullClassName.replace('.', '/'), "_proxy", Type.getDescriptor(BindingProxy.class));
		mv.visitInsn(Opcodes.RETURN);
		mv.visitMaxs(0, 0);
		mv.visitEnd();
		//}
	}
	
	private void generateSetterMethod(Method m, MethodVisitor mv) {
		mv.visitCode();
		mv.visitTypeInsn(Opcodes.NEW, "java/lang/UnsupportedOperationException");
		mv.visitInsn(Opcodes.DUP);
		mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/UnsupportedOperationException", "<init>", "()V");
		mv.visitInsn(Opcodes.ATHROW);
		mv.visitMaxs(0, 0);
	}
	
	private void generateGetterMethod(Method m, MethodVisitor mv) {
		mv.visitCode();
		Label l0 = new Label();
		Label l1 = new Label();
		Label l2 = new Label();
		mv.visitTryCatchBlock(l0, l1, l2, "java/lang/Exception");
		mv.visitLabel(l0);
		mv.visitLdcInsn(Type.getType(m.getDeclaringClass()));
		mv.visitLdcInsn(m.getName());

		if(m.getParameterTypes().length==0) {
			mv.visitInsn(Opcodes.ACONST_NULL);
		}
		else {
			mv.visitIntInsn(Opcodes.SIPUSH, m.getParameterTypes().length);
			mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Class");
			int i = 0;
			for (Class<?> paramType : m.getParameterTypes()) {
				mv.visitInsn(Opcodes.DUP);
				mv.visitIntInsn(Opcodes.SIPUSH, i);
				pushParamTypeToTypeArray(mv, paramType);
				mv.visitInsn(Opcodes.AASTORE);
				++i;
			}
		}
		
		mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getMethod", "(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;");
		mv.visitMethodInsn(Opcodes.INVOKESTATIC, BoundField.class.getName().replace('.', '/'), "setRef", "(Ljava/lang/reflect/Method;)V");
		mv.visitLabel(l1);
		Label l3 = new Label();
		mv.visitJumpInsn(Opcodes.GOTO, l3);
		mv.visitLabel(l2);
		mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] {"java/lang/Exception"});
		mv.visitVarInsn(Opcodes.ASTORE, 1);
		mv.visitLabel(l3);
		mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
		int returnOpCode = pushTypeDefaultValueAndReturnOpCode(mv, m.getReturnType());
		mv.visitInsn(returnOpCode);
		mv.visitMaxs(0, 0);
	}
	
	private void pushParamTypeToTypeArray(MethodVisitor mv, Class<?> paramType) {
		if(paramType.isPrimitive()) {
			Class<?> primitiveType = null;
			
			if(paramType.equals(Integer.TYPE)) {primitiveType=Integer.class;}
			else if(paramType.equals(Boolean.TYPE)) {primitiveType=Boolean.class;}
			else if(paramType.equals(Character.TYPE)) {primitiveType=Character.class;}
			else if(paramType.equals(Byte.TYPE)) {primitiveType=Byte.class;}
			else if(paramType.equals(Short.TYPE)) {primitiveType=Short.class;}
			else if(paramType.equals(Float.TYPE)) {primitiveType=Float.class;}
			else if(paramType.equals(Double.TYPE)) {primitiveType=Double.class;}
			else if(paramType.equals(Long.TYPE)) {primitiveType=Long.class;}
			else {throw new UnsupportedOperationException();}
			mv.visitFieldInsn(Opcodes.GETSTATIC, primitiveType.getName().replace('.','/'), "TYPE", "Ljava/lang/Class;");
		} else {
			mv.visitLdcInsn(Type.getType(paramType));
		}
	}
	
	private String getMethodDescriptor(Method m) {
		StringBuilder buf = new StringBuilder();
		buf.append('(');
		for (Class<?> paramType : m.getParameterTypes()) {
			buf.append(Type.getDescriptor(paramType));
		}
		buf.append(')');
		buf.append(Type.getDescriptor(m.getReturnType()));
		return buf.toString();
	}
	
	private int getAccessLevel(Method m) {
		if(Modifier.isPublic(m.getModifiers())) {
			return Opcodes.ACC_PUBLIC;
		}
		else if(Modifier.isProtected(m.getModifiers())) {
			return Opcodes.ACC_PROTECTED;
		}
		else if(Modifier.isPrivate(m.getModifiers())) {
			return Opcodes.ACC_PRIVATE;
		}
		return Opcodes.ACC_PROTECTED;
	}
	
	private int pushTypeDefaultValueAndReturnOpCode(MethodVisitor mv, Class<?> type) {
		int returnOpCode = Opcodes.IRETURN;
		if(type.equals(Boolean.TYPE)) {
			mv.visitIntInsn(Opcodes.BIPUSH,0);
		} else if(type.equals(Character.TYPE)) {
			mv.visitIntInsn(Opcodes.BIPUSH,0);
		} else if(type.equals(Byte.TYPE)) {
			mv.visitIntInsn(Opcodes.BIPUSH,0);
		} else if(type.equals(Short.TYPE)) {
			mv.visitInsn(Opcodes.ICONST_0);
		} else if(type.equals(Integer.TYPE)) {
			mv.visitInsn(Opcodes.ICONST_0);
		} else if(type.equals(Float.TYPE)) {
			mv.visitInsn(Opcodes.FCONST_0);
			returnOpCode=Opcodes.FRETURN;
		} else if(type.equals(Long.TYPE)) {
			mv.visitInsn(Opcodes.LCONST_0);
			returnOpCode=Opcodes.LRETURN;
		} else if(type.equals(Double.TYPE)) {
			mv.visitInsn(Opcodes.DCONST_0);
			returnOpCode=Opcodes.DRETURN;
		} else {
			mv.visitInsn(Opcodes.ACONST_NULL);
			returnOpCode=Opcodes.ARETURN;
		}
		return returnOpCode;
	}
}
